{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "学习多线程编程面临的最大的思维方式的转变有两点:\n",
    "- 当前线程可能随时会被切换出去，或者说被抢占(preempt)了。\n",
    "- 多线程程序中事件的发生顺序不再有全局统一的先后关系\n",
    "> 这意味着时空观的转变，从牛顿的绝对时空观转变为爱因斯坦的相对论时空观，分布式系统也面临类似的思维方式转变\n",
    "\n",
    "当线程被切换回来继续执行下一条语句（指令）的时候，全局数据（包括当前进程在操作系统内核中的状态）可能已经被其他线程修改了。\n",
    "\n",
    "例如，在没有为指针`p`加锁的情况下，`if（p && p->next）{ /* ...*/}`有可能导致 segfault，因为在逻辑与(`&&`)的前一个分支evaluate为 `true` 之后的一刹那，`p`可能被其他线程置为`NULL`或是被释放，后一个分支就访问了非法地址。\n",
    "\n",
    "在单CPU系统中，理论上我们可以通过记录 CPU上执行的指令的先后顺序来推演多线程的实际交织(interweaving)运行的情况。在多核系统中，多个线程是并行执行的，我们甚至没有统一的全局时钟来为每个事件编号。在没有适当同步的情况下，多个 CPU上运行的多个线程中的事件发生先后顺序是无法确定的。在引入适当同步后，事件之间才有了[happens-before](http://research.microsoft.com/en-us/um/people/lamport/pubs/time-clocks.pdf)关系。\n",
    "\n",
    "> - 在多CPU机器上，假设主板上两个物理CPU 的距离为15cm，CPU主频是2.4GHz，电信号在电路中的传播速度按 2×10<sup>8</sup>m/s估算，那么在1个时钟周期（0.42ns））之内，电信号不能从一个CPU到达另一个CPU。因此对于每个CPU自己这个观察者来说，它看到的事件发生的顺序没有全局一致性。\n",
    "\n",
    "多线程程序的正确性不能依赖于任何一个线程的执行速度，不能通过原地等待（`sleep()`）来假定其他线程的事件已经发生，而必须通过适当的同步来让当前线程能看到其他线程的事件的结果。无论线程执行得快与慢（被操作系统切换出去得越多，执行越慢），程序都应该能正常工作。例如下面这段代码就有这方面的问题。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [],
   "source": [
    "bool running = false; // 全局标志\n",
    "\n",
    "void threadFunc() {\n",
    "  while (running) {\n",
    "    // get task from queue\n",
    "  }\n",
    "} \n",
    "\n",
    "void start() {\n",
    "  muduo::Thread t(threadFunc);\n",
    "  t.start();\n",
    "  running = true; // 应该放到t.start()之前。\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这段代码暗中假定线程函数的启动慢于`runing`变量的赋值，因此线程函数能进入`while`循环执行我们想要的功能。如果上机测试运行这段代码，十有八九会按我们预期的那样工作。但到有一天，系统负载很高，`Thread::start()`调用`pthread_create()`陷人内核后返回时，内核决定换另外一个就绪任务来执行。于是`running`的赋值就推迟了，这时线程函数就可能不进入`while`循环而直接退出了。\n",
    "> 严格来说，全局`running`的赋值和读取应该用mutex或者 memory barrier，但不影响这里的讨论。\n",
    "\n",
    "或许有人会认为在 `while` 之前加一小段延时(sleep)就能解决问题，但这是错的，无论加多大的延时，系统都有可能先执行`while`的条件判断，然后再执行`running`的赋值。正确的做法是把`running`的赋值放到`t.start()`之前，这样借助`pthread_create()`的`happens-before`语意来保证`running`的新值能被线程看到。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 4.1 基本线程原语的选用\n",
    "\n",
    "我认为用C/C++编写跨平台的多线程程序不是普遍的需求，因此本书只谈现代Linux下的多线程编程(2004 年 Linux 2.6 内核发布之后，NPTL线程库)。`POSIX threads`的函数有110多个，真正常用的不过十几个。而且在C++程序中通常会有更为易用的wrapper，不会直接调用`Pthreads`函数。\n",
    "> 跨平台: 包括只针对 POSTX操作系统（Linux、Solaris、FreeBSD等等）的跨平台\n",
    "\n",
    "这 11个最基本的`Pthreads`函数是∶\n",
    "- 2个: 线程的创建和等待结束(`join`)。封装为`muduo::Thread`。\n",
    "- 4个: `mutex`的创建、销毁、加锁、解锁。封装为`muduo::MutexLock`。\n",
    "- 5个: 条件变量的创建、销毁、等待、通知、广播。封装为`muduo::Condition`。\n",
    "\n",
    "这些封装class都很直截了当，加起来也就一\n",
    "\n",
    "除此之外，`Pthreads`还提供了其他一些原语，有些是可以酌情使用的，有些则是不推荐使用的。\n",
    "\n",
    "可以酌情使用的有∶\n",
    "- `pthread_once`，封装为` muduo::Singleton<T>`。其实不如直接用全局变量。\n",
    "- `pthread_key*`，封装为` muduo::ThreadLocal<T>`。可以考虑用`__thread`替换之。\n",
    "\n",
    "不建议使用∶\n",
    "- `pthread_rwlock`，读写锁通常应慎用。muduo没有封装读写锁，这是有意的。\n",
    "- `sem_*`，避免用信号量（semaphore）。它的功能与条件变量重合，但容易用错。\n",
    "- `pthread_{cancel，kill}`。程序中出现了它们，则通常意味着设计出了问题。\n",
    "\n",
    "不推荐使用读写锁的原因是它往往造成提高性能的错觉（允许多个线程并发读），实际上在很多情况下，与使用最简单的mutex相比，它实际上降低了性能。另外，写操作会阻塞读操作，如果要求优化读操作的延迟，用读写锁是不合适的。\n",
    "\n",
    "多线程系统编程的难点不在于学习线程原语（primitives）、而在于理解多线程与现有的C/C++库函数和系统调用的交互关系，以进一步学习如何设计并实现线程安全且高效的程序。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 4.2 C/C++ 系统库的线程安全性\n",
    "\n",
    "现行的C/C++标准（C89/C99/C++03）并没有涉及线程，新版的C/C++标准（C11和C++11）规定了程序在多线程下的语意，C++11还定义了一个线程库(`std::thread`)。\n",
    "\n",
    "对于标准而言，关键的不是定义线程库，而是规定内存模型（memory model）。特别是规定一个线程对某个共享变量的修改何时能被其他线程看到，这称为内存序（memory ordering）或者内存能见度（memory visibility）。从理论上讲，如果没有合适的内存模型，编写正确的多线程程序属于撞大运行为，见 HansJ.Boehm 的论文[《Threads Cannot be Implemented as a Library》](http://www.hpl.hp.com/techreports/2004/HPL-2004-209.pdf)。不过我认为不必担心这篇文章提到的问题，标准的滞后不会对实践构成影响。因为从操作系统开始支持多线程到现在已经过去了近 20年，人们已经编写了不计其数的运行于关键生产环境的多线程程序，甚至 Linux操作系统内核本身也可以是抢占的（preemptive）。因此可以认为每个支持多线程的操作系统上自带的C/C++编译器对本平台的多线程支持都足够好。现在多线程程序工作不正常很难归结于编译器 bug，毕竟 POSIX threads 线程标准在 20世纪 90年代中期就制定了。当然，新标准的积极意义在于让编写跨平台的多线程程序更有保障了。\n",
    "\n",
    "Unix系统库（libc 和系统调用）的接口风格是在 20世纪 70年代早期确立的，而第一个支持用户态线程的 Unix操作系统出现在 20世纪 90年代早期。线程的出现立刻给系统函数库带来了冲击，破坏了20年来一贯的编程传统和假定。\n",
    "\n",
    "例如∶\n",
    "- `errno`不再是一个全局变量，因为每个线程可能会执行不同的系统库函数。\n",
    "- 有些\"纯函数\"不受影响，例如`memset/strcpy/snprintf`等等。\n",
    "- 有些影响全局状态或者有副作用的函数可以通过加锁来实现线程安全，例如`malloc/free, printf, fread/fseek`等等。\n",
    "- 有些返回或使用静态空间的函数不可能做到线程安全，因此要提供另外的版本，例如 `asctime_r/ctime_r/gmtime_r、stderror_r、strtok_r`等等。\n",
    "- 传统的 `fork()`并发模型不再适用于多线程程序（S4.9）。\n",
    "\n",
    "现在Linux glibc把`errno`定义为一个宏，注意`errno`是一个`lvalue`，因此不能简单定义为某个函数的返回值，而必须定义为对函数返回指针的 dereference。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [],
   "source": [
    "extern int *_errno_location(void);\n",
    "\n",
    "#define errno(*__errno_location())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "值得一提的是，操作系统支持多线程已有近 20年，早先一些性能方面的缺陷都基本被弥补了。例如最早的 SGI STL 自己定制了内存分配器，而现在g++自带的STL已经直接使用`malloc`来分配内存，`std::allocator`已经变成了鸡肋（S12.2）。原先 Google tcmalloc相对于glibc 2.3中的`ptmalloc2有`很大的性能提升，现在最新的 glibc 中的 `ptmalloc3`已经把差距大大缩小了。\n",
    "\n",
    "我们不必担心系统调用的线程安全性，因为系统调用对于用户态程序来说是原子的。但是要注意系统调用对于内核状态的改变可能影响其他线程，这个话题留到§4.6再细说。\n",
    "\n",
    "与直觉相反，POSIX标准列出的是一份非线程安全的函数的黑名单，而不是一份线程安全的函数的白名单（All functions defined by this volume of POSIX.1-2008 shall be thread-safe，except that the following functions need not be thread-safe）。在这份黑名单中，`system、getenv/putenv/setenv`等等函数都是不安全的。\n",
    "\n",
    "因此，可以说现在glibc库函数大部分都是线程安全的。特别是`FILE*`系列函数是安全的，glibc甚至提供了非线程安全的版本以应对某些特殊场合的性能需求。尽管单个函数是线程安全的，但两个或多个函数放到一起就不再安全了。例如 `fseek()`和 `fread()`都是安全的，但是对某个文件\"先seek再read\"这两步操作中间有可能会被打断，其他线程有可能趁机修改了文件的当前位置，让程序逻辑无法正确执行。在这种情况下，我们可以用 `flockfile(FILE*)`和 `funlockfile(FILE*)`函数来显式地加锁。并且由于`FILE*`的锁是可重入的，加锁之后再调用`fread()`不会造成死锁。\n",
    "> `fread_unlocked`、`fwrite_unlocked` 等等，见 `man unlocked_stdio`。\n",
    "\n",
    "如果程序直接使用`lseek(2)`和`read(2)`这两个系统调用来随机读取文件，也存在\"先seek再 read\"这种 race condition，但是似乎我们无法高效地对系统调用加锁。解决办法是改用`pread(2)`系统调用，它不会改变文件的当前位置。\n",
    "\n",
    "由此可见，编写线程安全程序的一个难点在于线程安全是不可组合的（composable），一个函数`foo()`调用了两个线程安全的函数，而这个 `foo()`函数本身很可能不是线程安全的。即便现在大多数glibc库函数是线程安全的，我们也不能像写单线程程序那样编写代码。\n",
    "> 就跟 C++ 异常安全也是不可组合的一样。\n",
    "\n",
    "例如，在单线程程序中，如果我们要临时转换时区，可以用`tzset()`函数，这个函数会改变程序全局的\"当前时区\"。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [],
   "source": [
    "// 获取伦敦的当前时间\n",
    "string oldTz = getenv(\"TZ\");  //save TZ,assumeing non-NULL\n",
    "putenv(\"TZ=Europe/London\");  //set TZ to London\n",
    "tzset();  //load London time zone\n",
    "\n",
    "struct tm localTimeInLN;\n",
    "time_t now = time(NULL);  //get time in UTC\n",
    "localtime_r(&now, &localTimeInLN);  //convert to London local time\n",
    "setenv(\"TZ\", oldTz.c_str(), 1);  //restore old TZ\n",
    "tzset();  //local old time zone"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "但是在多线程程序中，这么做是不安全的，即便`tzset()`本身是线程安全的。因为他改变了全局状态，这有可能影响其他线程转换当前时间，或者被其他进行类似的操作的线程影响。解决办法是使用`mudou::TimeZone class` ，每个immutable instance 对应一个时期，这样时间转换就不需要修改全局状态了。\n",
    "\n",
    "例如："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [],
   "source": [
    "class TimeZone\n",
    "{\n",
    "public:\n",
    "    explict TimeZone(const char* zonefile) ;\n",
    "    struct tm toLocalTime(time_t secondsSinceEpoch)const;\n",
    "    time_fromLocalTime(const struct tm&)const;\n",
    "    // default copy ctor /assigment /dtor are okay.\n",
    "    // ...\n",
    " };\n",
    "\n",
    " const TomeZone kNewYorkTz(\"/usr/share/zoneinfo/America/New_York\");\n",
    " const TomeZone kLondonTz(\"/usr/share/zoneinfo/Europe/London\");\n",
    "\n",
    " time_t now=time(NULL);\n",
    " struct tm locaTimeInNY = kNewYorkTz.toLcalTime(now);\n",
    " struct tm localTimeInLN = kLondonTz.toLocalTime(now);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "对于C/C++库的作者来说，如何设计线程安全的接口也成了一大考验，值得仿效的例子并不多。一个基本思路是尽量把 class 设计成 immutable 的，这样用起来就不必为线程安全操心了。\n",
    "\n",
    "尽管C++03标准没有明说标准库的线程安全性，但我们可以遵循一个基本原则∶凡是非共享的对象都是彼此独立的，如果一个对象从始至终只被一个线程用到，那么它就是安全的。另外一个事实标准是∶共享的对象的read-only操作是安全的，前提是不能有并发的写操作。\n",
    "> 这意味着标准库容器不能采用自调整（self-adjusting）的数据结构，比如 splay tree.这种数据结构在read 的时候也会修改状态，见https://www.cs.au.dk/~gerth/aa11/slides/selfadjusting.pdf\n",
    "\n",
    "例如两个线程各自访问自己的局部`vector`对象是安全的; 同时访问共享的`const vector`对象也是安全的，但是这个`vector`不能被第三个线程修改。一旦有writer，那么read-only操作也必须加锁，例如 `vector::size()`。\n",
    "\n",
    "根据§1.1.1对线程安全的定义，C++的标准库容器和 `std::string` 都不是线程安全的，只有 `std::allocator` 保证是线程安全的。一方面的原因是为了避免不必要的性能开销，另一方面的原因是单个成员函数的线程安全并不具备可组合性（composable ）。假设有`safe_vector<T> class`，它的接口与`std::vector`相同，不过每个成员函数都是线程安全的（类似Java synchronized方法）。但是用`safe_vector<T>`并不一定能写出线程安全的代码。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [],
   "source": [
    "safe_vector<int> vec; // 全局可见\n",
    "\n",
    "if(!vec.empty()) // 没有加锁保护\n",
    "{\n",
    "    int x = vec[0]; // 这两步在多线程下是不安全的\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在`if`语句判断 `vec` 非空之后，别的线程可能清空其元素，从而造成`vec[0]`失效。\n",
    "\n",
    "C++标准库中的绝大多数泛型算法是线程安全的、因为这些都是无状态纯函数。只要输入区间是线程安全的，那么泛型函数就是线程安全的。\n",
    "> `std::random_shuffle()`可能是个例外，它用到了随机数发生器。\n",
    "\n",
    "C++的 `iostream` 不是线程安全的，因为流式输出"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [],
   "source": [
    "std::cout << \"Now is\" << time(NULL);\n",
    "\n",
    "// 等价于两个函数调用\n",
    "\n",
    "std::cout.operator<<(\"Now is\")\n",
    "         .operator<<(time(NULL));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "即便`ostream::operator<<()`做到了线程安全，也不能保证其他线程不会在两次函数调用之前向stdout输出其他字符。\n",
    "\n",
    "对于\"线程安全的 stdout 输出\"这个需求，我们可以改用`printf`，以达到安全性和输出的原子性。但是这等于用了全局锁，任何时刻只能有一个线程调用printf，恐怕不见得高效。在多线程程序中高效的日志需要特殊设计，见第5章。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 4.3 Linux 上的线程标识\n",
    "\n",
    "POSIX threads 库提供了`pthread_self`函数用于返回当前进程的标识符，其类型为`pthread_t`。`pthread_t`不一定是一个数值类型（整数或指针），也有可能是一个结构体，因此 Pthreads 专门提供了`pthread_equal`函数用于对比两个线程标识符是否相等。\n",
    "\n",
    "这就带来一系列问题，包括∶\n",
    "- 无法打印输出 `pthread_t`，因为不知道其确切类型。也就没法在日志中用它表示当前线程的 `id`。\n",
    "- 无法比较 `pthread_t`的大小或计算其hash 值，因此无法用作关联容器的key。\n",
    "- 无法定义一个非法的 `pthread_t` 值，用来表示绝对不可能存在的线程 `id`，因此`MutexLock class `没有办法有效判断当前线程是否已经持有本锁。\n",
    "- `pthread_t`值只在进程内有意义，与操作系统的任务调度之间无法建立有效关联。比方说在`/proc`文件系统中找不到 `pthread_t` 对应的 task。\n",
    "\n",
    "另外，glibc的Pthreads实现实际上把 `pthread_t` 用作一个结构体指针（它的类型是 `unsigned long`），指向一块动态分配的内存，而且这块内存是反复使用的。这就造成`pthread_t`的值很容易重复。Pthreads 只保证同一进程之内，同一时刻的各个线程的`id`不同;不能保证同一进程先后多个线程具有不同的`id`，更不要说一台机器上多个进程之间的 `id`唯一性了。\n",
    "\n",
    "例如下面这段代码中先后两个线程的标识符是相同的∶"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "created thread id: 130492658628160\n",
      "thread running: 130492658628160\n",
      "created thread id: 130492658628160\n",
      "thread running: 130492658628160\n"
     ]
    }
   ],
   "source": [
    "#include <pthread.h>\n",
    "#include <stdio.h>\n",
    "\n",
    "void* threadFunc(void* arg)\n",
    "{\n",
    "    (void)arg;\n",
    "    printf(\"thread running: %lu\\n\", (unsigned long)pthread_self());\n",
    "    return NULL;\n",
    "}\n",
    "\n",
    "{\n",
    "    pthread_t t1, t2;\n",
    "\n",
    "    pthread_create(&t1, NULL, threadFunc, NULL);\n",
    "    printf(\"created thread id: %lu\\n\", (unsigned long)t1);\n",
    "    pthread_join(t1, NULL);\n",
    "\n",
    "    pthread_create(&t2, NULL, threadFunc, NULL);\n",
    "    printf(\"created thread id: %lu\\n\", (unsigned long)t2);\n",
    "    pthread_join(t2, NULL);\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "因此，`pthread_t`并不适合用作程序中对线程的标识符。\n",
    "\n",
    "在Linux上，我建议使用`gettid(2)`系统调用的返回值作为线程`id`，这么做的好处有∶\n",
    "- 它的类型是`pid_t`，其值通常是一个小整数, 便于在日志中输出。\n",
    "> 最大值是 `/proc/sys/kernel/pid_max`，默认情况下是 `32768`。\n",
    "- 在现代Linux中，它直接表示内核的任务调度 `id`，因此在`/proc`文件系统中可以轻易找到对应项∶`/proc/tid`或`/prod/pid/task/tid`。\n",
    "- 在其他系统工具中也容易定位到具体某一个线程，例如在 `top(1)`中我们可以按线程列出任务，然后找出 CPU使用率最高的线程`id`，再根据程序日志判断到底哪一个线程在耗用CPU。\n",
    "- 任何时刻都是全局唯一的，并且由于Linux分配新 pid采用递增轮回办法，短时间内启动的多个线程也会具有不同的线程 id。\n",
    "- `0`是非法值，因为操作系统第一个进程 init 的 `pid`是`1`。\n",
    "\n",
    "但是glibc并没有封装这个系统调用，需要我们自己实现。封装`gettid(2)`很简单，但是每次都执行一次系统调用似乎有些浪费，如何才能做到更高效呢?\n",
    "\n",
    "`muduo::CurrentThread::tid()`采取的办法是用`__thread`变量来缓存`gettid(2)`的返回值，这样只有在本线程第一次调用的时候才进行系统调用，以后都是直接从`thread local`缓存的线程`id` 拿到结果，效率无忧。多线程程序在打日志的时候可以在每一条日志消息中包含当前线程的`id`，不必担心有效率损失。读者有兴趣的话可以对比一下`boost::this_thread::get_id()`的实现效率。\n",
    "> 这个做法是受了 glibc 封装 `getpid()` 的启发。\n",
    "\n",
    "还有一个小问题，万一程序执行了`fork(2)`，那么子进程会不会看到 stale 的缓存结果呢?解决办法是用`pthread_atfork()`注册一个回调，用于清空缓存的线程 `id`。具体代码见 \n",
    "- [muduo/base/CurrentThread.h](../../muduo/muduo/base/CurrentThread.h)\n",
    "- [muduo/base/Thread.cc](../../muduo/muduo/base//Thread.cc)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 4.4 线程的创建与销毁的守则"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 创建\n",
    "\n",
    "线程的创建和销毁是编写多线程程序的基本要素，线程的创建比销毁要容易得多，只需要遵循几条简单的原则∶\n",
    "- 程序库不应该在未提前告知的情况下创建自己的\"背景线程\"\n",
    "> 所谓“背景线程”就是程序库在幕后自行启动、长期运行、与用户代码并发执行的辅助线程，用于处理库内部任务而不直接参与用户显式发起的操作。\n",
    "- 尽量用相同的方式创建线程，例如 `muduo::Thread`。\n",
    "- 在进入 `main()`函数之前不应该启动线程。\n",
    "- 程序中线程的创建最好能在初始化阶段全部完成。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "以下分别谈一谈这几个观点。\n",
    "\n",
    "线程是稀缺资源，一个进程可以创建的并发线程数目受限于地址空间的大小和内核参数，一台机器可以同时并行运行的线程数目受限于CPU的数目。因此我们在设计一个服务端程序的时候要精心规划线程的数目，特别是根据机器的 CPU数目来设置工作线程的数目，并为关键任务保留足够的计算资源。如果程序库在背地里使用了额外的线程来执行任务，我们这种资源规划就漏算了。可能会导致高估系统的可用资源，结果处理关键任务不及时，达不到预设的性能指标。\n",
    "\n",
    "还有一个重要原因是，一旦程序中有不止一个线程，就很难安全地 `fork()`了（§4.9）。因此\"库\"不能偷偷创建线程。如果确实有必要使用背景线程，至少应该让使用者知道。另外，如果有可能，可以让使用者在初始化库的时候传入线程池或event loop对象，这样程序可以统筹线程的数目和用途，避免低优先级的任务独占某个线程。\n",
    "\n",
    "理想情况下，程序里的线程都是用同一个 class创建的（`muduo::Thread`），这样容易在线程的启动和销毁阶段做一些统一的簿记（bookkeeping）工作。比如说调用一次 `muduo::CurrentThread::tid()`把当前线程`id`缓存起来，以后再取线程`id` 就不会陷入内核了。也可以统计当前有多少活动线程(线程数目可以从 `/proc/pid/status` 拿到。)，进程一共创建了多少线程，每个线程的用途分别是什么。C/C++的线程不像 Java线程那样有名字，但是我们可以通过 `Thread class`实现类似的效果。如果每个线程都是通过 `muduo::Thread` 启动的，这些都不难做到。必要的话可以写一个 `ThreadManager singleton class`，用它来记录当前活动线程，可以方便调试与监控。\n",
    "\n",
    "但是这不是总能做到的，有些第三方库（C语言库）会自己启动线程，这样的\"野生\"线程就没有纳入全局的`ThreadManager`管理之中。`muduo::CurrentThread::tid()`必须要考虑被这种\"野生\"线程调用的可能，因此它必须每次都检查缓存的线程 `id`是否有效，而不能假定在线程启动阶段已经缓存好了`id`。直接返回缓存值就行了。如果库提供异步回调，一定要明确说明会在哪个（哪些）线程调用用户提供的回调函数，这样用户可以知道在回调函数中能不能执行耗时的操作。会不会阳塞其他任务的执行。\n",
    "\n",
    "在`main()`函数之前不应该启动线程，因为这会影响全局对象的安全构造。我们知道，C++保证在进入`main()`之前完成全局对象的构造。同时，各个编译单元之间的对象构造顺序是不确定的，我们也有一些办法来影响初始化顺序，保证在初始化某个全局对象时使用到的其他全局对象都是构造完成的。但无论如何这些全局对象的构造是依次进行的，都在主线程中完成，无须考虑并发与线程安全。如果其中一个全局对象创建了线程，那就危险了。因为这破坏了初始化全局对象的基本假设。万一将来代码改动之后造成该线程访问了未经初始化的全局对象，那么这种隐晦错误查起来就很费劲了。或许你想用锁来保证全局对象初始化完成，但是怎么保证这个全局的锁对象的构造能在线程启动之前完成呢?因此，全局对象不能创建线程。如果一个库需要创建线程，那么应该进入`main()`函数之后再调用库的初始化函数去做。\n",
    "> 本章所指的\"全局对象\"也包括namespace级全局对象、文件级静态对象、class的静态对象，但不包括函数内的静态对象。\n",
    "\n",
    "不要为了每个计算任务，每次请求去创建线程。一般也不会为每个网络连接创建线程，除非并发连接数与CPU数相近。一个服务程序的线程数目应该与当前负载无关、而应该与机器的CPU数目有关，即 load average有比较小（最好不大于CPU数目）的上限。这样尽量避免出现 thrashing，不会因为负载急剧增加而导致机器失去正常响应。这么做的重要原因是，在机器失去响应期间，我们无法探查它究竟在做什么，也没办法立刻终止有问题的进程，防止损害进一步扩大。如果有实时性方面的要求，线程数目不应该超过CPU数目，这样可以基本保证新任务总能及时得到执行，因为总有CPU是空闲困的。最好在程序的初始化阶段创建全部工作线程，在程序运行期间不再创建或销毁线程。借助 `muduo::ThreadPool`和 `muduo::EventLoop`，我们很容易就能把计算任务和IO任务分配到已有的线程，代价只有新建线程的几分之一。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 销毁\n",
    "\n",
    "线程的[销毁](https://blog.csdn.net/program_think/article/details/3991107)有几种方式\n",
    "\n",
    "- 自然死亡。从线程主函数返回，线程正常退出。\n",
    "- 非正常死亡。从线程主函数抛出异常或线程触发 segfault 信号等非法操作\n",
    "> 通常伴随进程死亡。如果程序中的某个线程意外终止，我不认为让进程继续带伤运行下去有何必要。\n",
    "- 自杀。在线程中调用 `pthread_exit()`来立刻退出线程。\n",
    "- 他杀。其他线程调用 `pthread_cancel()`来强制终止某个线程。\n",
    "\n",
    "`pthread_kill()`是往线程发信号，留到§4.10再讨论。\n",
    "\n",
    "线程正常退出的方式只有一种，即自然死亡。任何从外部强行终止线程的做法和想法都是[错的](https://yupo5656.hatenadiary.org/search?q=UNIX%E4%B8%8A%E3%81%A7%E3%81%AEC%2B%2B%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%E8%A8%AD%E8%A8%88%E3%81%AE%E5%AE%9A%E7%9F%B3)。\n",
    "\n",
    "佐证有∶Java的Thread class把 `stop()、suspend()、destroy()`等函数都废弃（deprecated）了，Boost.Threads 根本就不提供 [`thread::cancel()`成员函数](https://www.boost.org/doc/libs/1_34_0/doc/html/thread/faq.html#id1746521)。因为强行终止线程的话（无论是自杀还是他杀），它没有机会清理资源。也没有机会释放已经持有的锁，其他线程如果再想对同一个 mutex 加锁，那么就会立刻死锁。因此我认为不用去研究 cancellation point 这种\"鸡肋\"概念。\n",
    "\n",
    "如果确实需要强行终止一个耗时很长的计算任务，而又不想在计算期间周期性地检查某个全局退出标志，那么可以考虑把那一部分代码 `fork()`为新的进程，这样杀(`kill(2)`)一个进程比杀本进程内的线程要安全得多。当然，`fork()`的新进程与本进程的通信方式也要慎重选取，最好用文件描述符(`pipe(2)/socketpair(2)/TCPsocket`)来收发数据，而不要用共享内存和跨进程的互斥器等IPC，因为这样仍然有死锁的可能。\n",
    "\n",
    "`muduo::Thread`不是传统意义上的 RAII class，因为它析构的时候没有销毁持有的`Pthreads` 线程句柄（`pthread_t`），也就是说`Thread` 的析构不会等待线程结束。一般而言，我们会让`Thread` 对象的生命期长于线程，然后通过`Thread::join()`来等待线程结束并释放线程资源。如果 `Thread` 对象的生命期短于线程，那么就没有机会释放 `pthread_t`了。`muduo::Thread`没有提供`detach()`成员函数，因为我不认为这是必要的。\n",
    "\n",
    "最后，我认为如果能做到前面提到的\"程序中线程的创建最好能在初始化阶段全部完成\"，则线程是不必销毁的，伴随进程一直运行，彻底避开了线程安全退出可能面临的各种困难，包括 `Thread` 对象生命期管理、资源释放等等。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## pthread_cancel 与 C++\n",
    "\n",
    "POSIX threads有cancellation point（取消点）这个概念，意思是线程执行到取消点有可能会被终止（cancel）（如果别的线程对它调用了`pthread_cancel()`的话）POSIX标准列出了必须或者可能是cancellation point的函数\n",
    "> - https://stackoverflow.com/questions/433989/what-are-the-posix-cancellation-points\n",
    "> - https://pubs.opengroup.org/onlinepubs/000095399/functions/xsh_chap02_09.html#tag_02_09_05_02\n",
    "\n",
    "在C++中，cancellation point的实现与C语言有所不同，线程不是执行到此函数就立刻终止，而是该函数会抛出异常。这样可以有机会执行stack unwind，析构栈上对象（特别是释放持有的锁）。如果一定要使用 cancellation point，建议读一读Ulrich Drepper写的 [Cancellation and C++Exceptions](https://udrepper.livejournal.com/21541.html) 这篇短文。不过按我的观点，不应该从外部杀死线程。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## exit(3)在C++中不是线程安全的\n",
    "\n",
    "`exit(3)`函数在C++中的作用除了终止进程，还会析构全局对象和已经构造完的函数静态对象。这有潜在的死锁可能，考虑下面这个例子。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [],
   "source": [
    "void someFunctionMayCallExit()\n",
    "{\n",
    "    exit(1);\n",
    "}\n",
    " \n",
    "class GlobalObject   //:boost::noncopyable\n",
    "{\n",
    "public:\n",
    "    void doit(){\n",
    "        MutexLockGuard lock(mutex_);\n",
    "        someFunctionMayCallExit();\n",
    "    }\n",
    " \n",
    "    ~GlobalObject(){\n",
    "        printf(\"GlobalObject:~GlobalObject\\n\");\n",
    "        MutexLockGuard lock(mutex_);\n",
    "        //clean up\n",
    "        printf(\"GlobalObject:~GlobalObject cleanning\\n\");\n",
    "    }\n",
    "private:\n",
    "    MutexLock mutex_;\n",
    "};\n",
    " \n",
    "GlobalObject g_obj;\n",
    " \n",
    "int main()\n",
    "{\n",
    "    g_obj.doit();\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`GlobalObject::doit()`函数辗转调用了`exit()`，从而触发了全局对象`g_obj`的析构。`GlobalObject`的析构函数会试图加锁`mutex_`，而此时 `mutex_`已经被`GlobalObject::doit()`锁住了，于是造成了死锁。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "再举一个调用纯虚函数导致程序崩溃的例子。假如有一个策略基类，在运行时我们会根据情况使用不同的无状态策略（派生类对象）。由于策略是无状态的，因此可以共享派生类对象，不必每次都新建。这里以日历（Calendar）基类和不同国家的假期（AmericanCalendar和 BritishCalendar）为例，`factory`函数返回某个全局对象的引用，而不是每次都创建新的派生类对象。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [],
   "source": [
    "class Calendar:boost::noncopyable\n",
    "{\n",
    "public:\n",
    "    //传入一个日期，检测是否为假日\n",
    "    virtual bool isHoliday(muduo::Date d) const=0;\n",
    "    virtual ~ Calendar(){}\n",
    "};\n",
    " \n",
    "class AmericanCalendar:public Calendar\n",
    "{\n",
    "public:\n",
    "    virtual bool isHoliday(muduo::Date d)const;\n",
    "};\n",
    " \n",
    "class BritishCalendar:public Calendar\n",
    "{\n",
    "public:\n",
    "    virtual bool isHoliday(muduo::Date d)const;\n",
    "};\n",
    "\n",
    "AmericanCalendar americanCalendar; //全局对象\n",
    "BritishCalendar britishCalendar;\n",
    " \n",
    "//在其中创建AmericanCalendar或BritishCalendar对象，并返回\n",
    "Calendar& getCalendar(const std::string& region);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "通常的使用方式是通过 factory拿到具体国家的日历，再判断某一天是不是假期∶"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [],
   "source": [
    "void processRequest(const Request& req)\n",
    "{\n",
    "    Calendar& calendar = getCalendar(req.region);\n",
    "    // 如果别的线程在此时调用了 exit() ……\n",
    "    if(calendar.isHoliday(req.settlement_date))\n",
    "    {\n",
    "        // do something\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这一切都工作得很好，直到有一天我们想主动退出这个服务程序，于是某个线程调用了`exit()`，析构了全局对象，结果造成另一个线程在调用`Calendar::isHoliday`时发生崩溃∶"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "pure virtual method called\n",
    "terminate called without an active exception\n",
    "Aborted (core dumped)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "当然，这只是举例说明\"用全局对象实现无状态策略\"在多线程中析构可能有危险。在真实的项目中，`Calendar` 应该在运行的时候从外部配置读入，而不能写死在代码中。\n",
    "> 比如，中国每年几大节日放假安排要等到头一年年底才由国务院假日办公布。又比如 2012年英女王登基60 周年，英国新加了一两个节日。\n",
    "\n",
    "这其实不是`exit()`的过错，而是全局对象析构的问题。C++标准没有照顾全局对象在多线程环境下的析构，据我看似乎也没有更好的办法。如果确实需要主动结束线程，则可以考虑用`_exit(2)`系统调用。它不会试图析构全局对象，但是也不会执行其他任何清理工作，比如 `flush`标准输出。\n",
    "\n",
    "由此可见，安全地退出一个多线程的程序并不是一件容易的事情。何况这里还没有涉及如何安全地退出其他正在运行的线程。这需要精心设计共享对象的析构顺序防止各个线程在退出时访问已失效的对象。在编写长期运行的多线程服务程序的时候，可以不必追求安全地退出，而是让进程进入拒绝服务状态，然后就可以直接杀掉了（9.3）。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 4.5 善用`__thread`关键字\n",
    "\n",
    "`__thread` 是GCC 内置的线程局部存储设施（thread local storage ）。它的实现非常高效，比 `pthread_key_t` 快很多，见 Ulrich Drepper写的[《ELF Handling For Thread-Local Storage 》](http://www.akkadia.org/drepper/tls.pdf)。\n",
    "\n",
    "`__thread`变量的存取效率可与全局变量相比∶"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [],
   "source": [
    "// https://godbolt.org/z/bhY3b3e6Y\n",
    "\n",
    "int g_var;          // 全局变量\n",
    "__thread int t_var; // __thread变量\n",
    "\n",
    "void foo() {\n",
    "    g_var = 1;\n",
    "    t_var = 2;\n",
    "}"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {
    "vscode": {
     "languageId": "raw"
    }
   },
   "source": [
    "g_var:\n",
    "  .zero 4\n",
    "t_var:\n",
    "  .zero 4\n",
    "foo():\n",
    "  push ebp\n",
    "  mov ebp, esp\n",
    "  mov DWORD PTR g_var, 1           # g_var 直接寻址\n",
    "  mov DWORD PTR gs:t_var@ntpoff, 2 # t_var 也是直接寻址, 用了段寄存器gs\n",
    "  nop\n",
    "  pop ebp\n",
    "  ret"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[`__thread`使用规则](https://gcc.gnu.org/onlinedocs/gcc/Thread-Local.html)：只能用于修饰POD类型，不能修饰class类型，因为无法自动调用构造函数和析构函数。`__thread`可以用于修饰全局变量、函数内的静态变量，但是不能用于修饰函数的局部变量或者class的普通成员变量。另外，`__thread`变量的初始化只能用编译期常量。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [],
   "source": [
    "__thread string t_obj1(\"Chen Shuo\");  // 错误，不能调用对象的构造函数\n",
    "__thread string *t_obj2 = new string; // 错误，初始化必须用编译期常量\n",
    "__thread string *t_obj3 = NULL; // 正确，但是需要手工初始化并销毁对象"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`__thread`变量是每个线程有一份独立实体，各个线程的变量值互不干扰。除了这个主要用途，它还可以修饰那些“值可能会变，带有全局性，但是又不值得用全局锁保护”的变量。\n",
    "\n",
    "muduo代码中用到了好几处`__thread`，简单列举如下：\n",
    "- `muduo/base/Logging.cc`缓存最近一条日志时间的年月日时分秒，如果一秒之内输出多条日志，可避免重复格式化。另外，`muduo::strerror_tl`把`strerror_r(3)`做成如同`strerror(3)`一样好用，而且是线程安全的。\n",
    "- `muduo/base/ProcessInfo.cc`用线程局部变量来简化`::scandir(3)`的使用。\n",
    "- `muduo/base/Thread.cc`缓存每个线程的id。\n",
    "- `muduo/net/EventLoop.cc`用于判断当前线程是否只有一个EventLoop对象。\n",
    "\n",
    "以上例子都是`__thread`修饰POD类型的变量。\n",
    "\n",
    "如果要用到`thread local`的class对象，可以考虑使用`muduo::ThreadLocal<T>`和`muduo::ThreadLocalSingleton<T>`这两个class，它能在线程退出时销毁class对象。例如`examples/asio/chat/server_threaded_highperformance.cc`用`ThreadLocalSingleton`来保存每个`EventLoop`线程所管辖的客户连接，以实现高效的消息转发(p.260)。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> C++11有`thread_local`来代替"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [],
   "source": [
    "// https://godbolt.org/z/eza4aEb8d\n",
    "\n",
    "#include <iostream>\n",
    "#include <thread>\n",
    "#include <vector>\n",
    "\n",
    "// C++11 标准线程局部存储\n",
    "thread_local int tls_counter = 0;\n",
    "\n",
    "void worker(int id) {\n",
    "    for (int i = 0; i < 3; ++i) {\n",
    "        ++tls_counter;  // 每个线程都有独立的 tls_counter 副本\n",
    "        std::cout << \"Thread \" << id\n",
    "                  << \" iteration \" << i\n",
    "                  << \" tls_counter = \" << tls_counter << '\\n';\n",
    "    }\n",
    "}\n",
    "\n",
    "int main() {\n",
    "    std::vector<std::thread> threads;\n",
    "    for (int i = 0; i < 3; ++i) {\n",
    "        threads.emplace_back(worker, i);\n",
    "    }\n",
    "    for (auto& t : threads) {\n",
    "        t.join();\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 4.6 多线程与IO\n",
    "\n",
    "这可算是本章最为关键的一节。本书只讨论同步IO，包括阻塞与非阻塞，不讨论异步IO(AIO)。在进行多线程网络编程的时候，几个自然的问题是：如何处理IO？能否多个线程同时读写同一个socket文件描述符(没有特殊说明时，指 TCP socket)？我们知道用多线程同时处理多个socket通常可以提高效率，那么用多线程处理同一个socket也可以提高效率吗？\n",
    "\n",
    "首先，操作文件描述符的系统调用本身是线程安全的，我们不用担心多个线程同时操作文件描述符会造成进程崩溃或内核崩溃。\n",
    "\n",
    "但是，多个线程同时操作同一个socket文件描述符确实很麻烦，我认为是得不偿失的。需要考虑的情况如下：\n",
    "- 如果一个线程正在阻塞地`read(2)`某个socket，而另一个线程`close(2)`了此socket。\n",
    "- 如果一个线程正在阻塞地`accept(2)`某个listening socket，而另一个线程`close(2)`了此socket。\n",
    "- 更糟糕的是，一个线程正准备`read(2)`某个socket，而另一个线程`close(2)`了此socket；第三个线程又恰好`open(2)`了另一个文件描述符，其fd号码正好与前面的socket相同。这样程序的逻辑就混乱了(§ 4.7)。\n",
    "\n",
    "我认为以上这几种情况都反映了程序逻辑设计上有问题。\n",
    "\n",
    "现在假设不考虑关闭文件描述符，只考虑读和写，情况也不见得多好。因为socket读写的特点是不保证完整性，读`100`字节有可能只返回`20`字节，写操作也是一样的\n",
    "- 如果两个线程同时`read`同一个TCP socket，两个线程几乎同时各自收到一部分数据，如何把数据拼成完整的消息？如何知道哪部分数据先到达？\n",
    "- 如果两个线程同时`write`同一个TCP socket，每个线程都只发出去半条消息，那接收方收到数据如何处理？\n",
    "- 如果给每个TCP socket配一把锁，让同时只能有一个线程读或写此socket，似乎可以“解决”问题，但这样还不如直接始终让同一个线程来操作此socket来得简单。\n",
    "- 对于非阻塞IO，情况是一样的，而且收发消息的完整性与原子性几乎不可能用锁来保证，因为这样会阻塞其他IO线程。\n",
    "\n",
    "如此看来，理论上只有`read`和`write`可以分到两个线程去，因为TCP socket是双向IO。问题是真的值得把`read`和`write`拆开成两个线程吗？\n",
    "\n",
    "以上讨论的都是网络IO，那么多线程可以加速磁盘IO吗？首先要避免`lseek(2)/read(2)`的race condition(§ 4.2)。做到这一点之后，据我看，用多个线程`read`或`write`同一个文件也不会提速。不仅如此，多个线程分别`read`或`write`同一个磁盘上的多个文件也不见得能提速。因为每块磁盘都有一个操作队列，多个线程的读写请求到了内核是排队执行的。只有在内核缓存了大部分数据的情况下，多线程读这些热数据才可能比单线程快。多线程磁盘IO的一个思路是每个磁盘配一个线程，把所有针对此磁盘的IO都挪到同一个线程，这样或许能避免或减少内核中的锁争用。我认为应该用“显然是正确”的方式来编写程序，一个文件只由一个进程中的一个线程来读写，这种做法显然是正确的。\n",
    "\n",
    "为了简单起见，我认为多线程程序应该遵循的原则是：每个文件描述符只由一个线程操作，从而轻松解决消息收发的顺序性问题，也避免了关闭文件描述符的各种race condition。一个线程可以操作多个文件描述符，但一个线程不能操作别的线程拥有的文件描述符。这一点不难做到，muduo网络库已经把这些细节封装了\n",
    "\n",
    "`epoll`也遵循相同的原则。Linux文档并没有说明：当一个线程正阻塞在`epoll_wait()`上时，另一个线程往此`epoll fd`添加一个新的监视fd会发生什么。新`fd`上的事件会不会在此次`epoll_wait()`调用中返回？为了稳妥起见，我们应该把对同一个`epoll fd`的操作（添加、删除、修改、等待）都放到同一个线程中执行，这正是我们需要`muduo::EventLoop::wakeup()`的原因。\n",
    "\n",
    "当然，一般的程序不会直接使用`epoll`、`read`、`write`，这些底层操作都由网络库代劳了。\n",
    "\n",
    "这条规则有两个例外：\n",
    "- 对于磁盘文件，在必要的时候多个线程可以同时调用`pread(2)`/`pwrite(2)`来读写同一个文件；\n",
    "- 对于UDP，由于协议本身保证消息的原子性，在适当的条件下（比如消息之间彼此独立）可以多个线程同时读写同一个UDP文件描述符。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 4.7 用 RAII 包装文件描述符\n",
    "\n",
    "本节谈一谈在多线程程序中如何管理文件描述符。Linux的文件描述符(file descriptor)是小整数，在程序刚刚启动的时候，`0`是标准输入，`1`是标准输出，`2`是标准错误。这时如果我们新打开一个文件，它的文件描述符会是`3`，因为POSIX标准要求每次新打开文件（含socket）的时候必须使用当前最小可用的文件描述符号码。\n",
    "\n",
    "POSIX这种分配文件描述符的方式稍不注意就会造成串话。\n",
    "\n",
    "比如前面举过的例子，一个线程正准备`read(2)`某个socket，而第二个线程几乎同时`close(2)`了此socket；第三个线程又恰好`open(2)`了另一个文件描述符，其号码正好与前面的socket相同（因为比它小的号码都被占用了）。这时第一个线程可能会读到不属于它的数据，不仅如此，还把第三个线程的功能也破坏了，因为第一个线程把数据读走了（TCP连接的数据只能读一次，磁盘文件会移动当前位置）。\n",
    "\n",
    "另外一种情况，一个线程从`fd=8`收到了比较耗时的请求，它开始处理这个请求，并记住要把响应结果发给`fd=8`。但是在处理过程中，`fd=8`断开连接，被关闭了，又有新的连接到来，碰巧使用了相同的`fd=8`。当线程完成响应的计算，把结果发给`fd=8`时，接收方已经物是人非，后果难以预料。\n",
    "\n",
    "在单线程程序中，或许可以通过某种全局表来避免串话；在多线程程序中，我不认为这种做法会是高效的（通常意味着每次读写都要对全局表加锁）。\n",
    "\n",
    "在C++里解决这个问题的办法很简单：RAII。用Socket对象包装文件描述符，所有对此文件描述符的读写操作都通过此对象进行，在对象的析构函数里关闭文件描述符。这样一来，只要Socket对象还活着，就不会有其他Socket对象跟它有一样的文件描述符，也就不可能串话。剩下的问题就是做好多线程中的对象生命期管理，这在第1章已经完美解决了。\n",
    "\n",
    "引申问题：为什么服务端程序不应该关闭标准输出(`fd=1`)和标准错误(`fd=2`)? 因为有些第三方库在特殊紧急情况下会往`stdout`或`stderr`打印出错信息，如果我们的程序关闭了标准输出(`fd=1`)和标准错误(`fd=2`)，这两个文件描述符有可能被网络连接占用，结果造成对方收到莫名其妙的数据。正确的做法是把`stdout`或`stderr`重定向到磁盘文件（最好不要是`/dev/null`），这样我们不至于丢失关键的诊断信息。当然，这应该由启动服务程序的看门狗进程完成，对服务程序本身是透明的。\n",
    "> [http://github.com/chenshuo/muduo-protorpc](http://github.com/chenshuo/muduo-protorpc)中Zurg slave示例\n",
    "\n",
    "现代C++的一个特点是对象生命期管理的进步，体现在不需要手工`delete`对象。在网络编程中，有的对象是长命的（例如`TcpServer`），有的对象是短命的（例如`TcpConnection`）。长命的对象的生命期往往和整个程序一样长，那就很容易处理，直接使用全局对象（或`scoped_ptr`）或者做成`main()`的栈上对象都行。对于短命的对象，其生命期不一定完全由我们控制，比如对方客户端断开了某个TCP socket，它对应的服务端进程中的`TcpConnection`对象（其必然是个`heap`对象，不可能是`stack`对象）的生命也即将走到尽头。但是这时我们并不能立刻`delete`这个对象，因为其他地方可能还持有它的引用，贸然`delete`会造成空悬指针。只有确保其他地方没有持有该对象的引用的时候，才能安全地销毁对象，这自然会用到引用计数。在多线程程序中，安全地销毁对象不是一件轻而易举的事情，见第1章。\n",
    "\n",
    "在非阻塞网络编程中，我们常常要面临这样一种场景：从某个TCP连接A收到了一个request，程序开始处理这个request；处理可能要花一定的时间，为了避免耽误（阻塞）处理其他request，程序记住了发来request的TCP连接，在某个线程池中处理这个请求；在处理完之后，会把response发回TCP连接A。但是，在处理request的过程中，客户端断开了TCP连接A，而另一个客户端刚好创建了新连接B。我们的程序不能只记住TCP连接A的文件描述符，而应该持有封装socket连接的`TcpConnection`对象，保证在处理request期间TCP连接A的文件描述符不会被关闭。或者持有`TcpConnection`对象的弱引用(`weak_ptr`)，这样能知道socket连接在处理request期间是否已经关闭了，`fd=8`的文件描述符到底是“前世”还是“今生”。\n",
    "\n",
    "否则的话，旧的TCP连接A一断开，`TcpConnection`对象销毁，关闭了旧的文件描述符(RAII)，而且新连接B的socket文件描述符有可能等于之前断开的TCP连接（这是完全可能的，POSIX要求每次新建文件描述符时选取当前最小的可用的整数）。当程序处理完旧连接的request时，就有可能把response发给新的TCP连接B，造成串话\n",
    "\n",
    "为了应对这种情况，防止访问失效的对象或者发生网络串话，muduo使用`shared_ptr`来管理`TcpConnection`的生命期。这是唯一一个采用引用计数方式管理生命期的对象。如果不用`shared_ptr`，我想不出其他安全且高效的办法来管理多线程网络服务端程序中的并发连接。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 4.8 RAII与fork()\n",
    "\n",
    "在编写C++程序的时候，我们总是设法保证对象的构造和析构是成对出现的，否则就几乎一定会有内存泄漏。在现代C++中，这一点不难做到(§ 1.7)。利用这一特性，我们可以用对象来包装资源，把资源管理与对象生命期管理统一起来(RAII)。但是，假如程序会`fork()`，这一假设就会被破坏了。考虑下面这个例子，`Foo`对象构造了一次，但是析构了两次。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "cpp"
    }
   },
   "outputs": [],
   "source": [
    "int main()\n",
    "{\n",
    "    Foo foo;    //调用构造函数\n",
    "    fork();     //fork为两个进程\n",
    "    foo.doit(); //在父子进程中都使用foo\n",
    " \n",
    "    // 析构函数会被调用两次，父进程和子进程各一次\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果`Foo class`封装了某种资源，而这个资源没有被子进程继承，那么`Foo::doit()`的功能在子进程中是错乱的。而我们没有办法自动预防这一点，总不能每次申请一个资源就去调用一次`pthread_atfork()`吧？\n",
    "\n",
    "`Fork()`之后，子进程继承了父进程的几乎全部状态，但也有少数例外。子进程会继承地址空间和文件描述符，因此用于管理动态内存和文件描述符的`RAII class`都能正常工作。\n",
    "\n",
    "但是子进程不会继承：\n",
    "- 父进程的内存锁，`mlock(2)`、`mlockall(2)`。\n",
    "- 父进程的文件锁，`fcntl(2)`。\n",
    "- 父进程的某些定时器，`setitimer(2)`、`alarm(2)`、`timer_create(2)`等等。\n",
    "- 其他，见`man 2 fork`。\n",
    "\n",
    "通常我们会用RAII手法来管理以上种类的资源（加锁解锁、创建销毁定时器等等），但是在`fork()`出来的子进程中不一定正常工作，因为资源在`fork()`时已经被释放了。比方说用RAII技法封装`timer_create()/timer_delete()`，在子进程中析构函数调用`timer_delete()`可能会出错，因为试图释放一个不存在的资源。或者更糟糕地把其他对象持有的`timer`给释放了（如果碰巧新建的timer_t与之重复的话）。\n",
    "\n",
    "因此，我们在编写服务端程序的时候，“是否允许`fork()`”是在一开始就应该慎重考虑的问题，在一个没有为`fork()`做好准备的程序中使用`fork()`，会遇到难以预料的问题。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 4.9 多线程与fork()\n",
    "\n",
    "多线程与`fork()`的协作性很差。这是POSIX系列操作系统的历史包袱。因为长期以来程序都是单线程的，`fork()`运转正常。当20世纪90年代初期引入多线程之后，`fork()`的适用范围大为缩减。\n",
    "> 在现代Linux glibc中，`fork(3)`不是直接使用 `fork(2)`系统调用，而是使用`clone(2)` syscall，不过不影响这里的讨论。\n",
    "\n",
    "`fork()`一般不能在多线程程序中调用，因为Linux的`fork()`只克隆当前线程的thread of control，不克隆其他线程。`fork()`之后，除了当前线程之外，其他线程都消失了。也就是说不能一下子`fork()`出一个和父进程一样的多线程子进程。Linux没有`forkall()`这样的系统调用，`forkall()`其实也是很难办的（从语意上），因为其他线程可能等在condition variable上，可能阻塞在系统调用上，可能等着mutex以跨入临界区，还可能在密集的计算中，这些都不好全盘搬到子进程里。\n",
    "> - http://www.linuxprogrammingblog.com/threads-and-fork-think-twice-before-using-them\n",
    "> - http://www.cppblog.com/lymons/archive/2008/06/01/51836.html\n",
    "\n",
    "`fork()`之后子进程中只有一个线程，其他线程都消失了，这就造成一个危险的局面。其他线程可能正好位于临界区之内，持有了某个锁，而它突然死亡，再也没有机会去解锁了。如果子进程试图再对同一个mutex加锁，就会立刻死锁。在`fork()`之后，子进程就相当于处于signal handler之中，你不能调用线程安全的函数（除非它是可重入的），而只能调用异步信号安全(async-signal-safe)的函数。\n",
    "\n",
    "比方说，`fork()`之后，子进程不能调用：\n",
    "- `malloc(3)`。因为`malloc()`在访问全局状态时几乎肯定会加锁。\n",
    "- 任何可能分配或释放内存的函数，包括`new`、`map::insert()`、`snprintf` ……\n",
    "> 在浮点数转换为字符串的时候有可能需要动态分配内存。\n",
    "- 任何Pthreads函数。你不能用p`thread_cond_signal()`去通知父进程，只能通过读写`pipe(2)`来同步。\n",
    "> 见 http://github.com/chenshuo/muduo-protorpc 中 Zurg slave 示例的 `Process::start()`。\n",
    "- `printf()`系列函数，因为其他线程可能恰好持有stdout/stderr的锁。\n",
    "- 除了`man 7 signal`中明确列出的“signal安全”函数之外的任何函数。\n",
    "\n",
    "照此看来，唯一安全的做法是在`fork()`之后立即调用`exec()`执行另一个程序，彻底隔断子进程与父进程的联系。\n",
    "\n",
    "不得不说，同样是创建进程，Windows的`CreateProcess()`函数的顾虑要少得多，因为它创建的进程跟当前进程关联较少。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 4.10 多线程与 signal\n",
    "\n",
    "Linux/Unix的信号(signal)与多线程可谓是[水火不容](http://www.linuxprogrammingblog.com/all-about-linux-signals?page=11)。在单线程时代，编写信号处理函数(signal handler)就是一件棘手的事情，由于signal打断了正在运行的thread of control，在signal handler中只能调用`async-signal-safe`的[函数](http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03)，即所谓的“可重入(reentrant)”函数，就好比在DOS时代编写中断处理例程[(ISR)](https://en.wikipedia.org/wiki/Interrupt_handler)一样。不是每个线程安全的函数都是可重入的，见§ 4.9举的例子。\n",
    "\n",
    "还有一点，如果signal handler中需要修改全局数据，那么被修改的变量必须是`sig_atomic_t`类型的。否则被打断的函数在恢复执行后很可能不能立刻看到signal handler改动后的数据，因为编译器有可能假定这个变量不会被他处修改，从而优化了内存访问。\n",
    "\n",
    "在多线程时代，signal的语义更为复杂。信号分为两类：发送给某一线程(SIG-SEGV)，发送给进程中的任一线程(SIGTERM)，还要考虑掩码(mask)对信号的屏蔽等。特别是在signal handler中不能调用任何`Pthreads`函数，不能通过condition variable来通知其他线程。\n",
    "\n",
    "在多线程程序中，使用signal的第一原则是不要使用signal。包括\n",
    "- 不要用signal作为IPC的手段，包括不要用SIGUSR1等信号来触发服务端的行为。如果确实需要，可以用§ 9.5介绍的增加监听端口的方式来实现双向的、可远程访问的进程控制。\n",
    "- 也不要使用基于signal实现的定时函数，包括alarm/ualarm/setitimer/timer_create、sleep/usleep等等。\n",
    "- 不主动处理各种异常信号（SIGTERM、SIGINT等等），只用默认语义：结束进程。有一个例外：SIGPIPE，服务器程序通常的做法是忽略此信号，否则如果对方断开连接，而本机继续`write`的话，会导致程序意外终止。\n",
    "> 在命令行程序中，默认的SIGPIPE行为非常有用。例如查看日志中的前10条错误信息，可以用管道将命令串起来∶`gunzip-c log.gz | grep ERROR | head`，由于head关闭了管道的写人端，grep会遇到SIGPIPE而终止，同理 gunzip也就不需要解压缩整个巨大的日志文件。这也可能是 Unix默认使用阻塞IO的历史原因之一。\n",
    "- 在没有别的替代方法的情况下（比方说需要处理SIGCHLD信号），把异步信号转换为同步的文件描述符事件。传统的做法是在signal handler里往一个特定的pipe(2)写一个字节，在主程序中从这个pipe读取，从而纳入统一的IO事件处理框架中去。现代Linux的做法是采用signalfd(2)把信号直接转换为文件描述符事件，从而从根本上避免使用signal handler 。\n",
    "> [http://github.com/chenshuo/muduo-protorpc](http://github.com/chenshuo/muduo-protorpc)中Zurg slave示例的ChildManager class"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 4.11 Linux新增系统调用的启示\n",
    "\n",
    "本节的内容源自我的一篇[同名博客](https://blog.csdn.net/Solstice/article/details/5327881)，省略了`signalfd`、`timerfd`、`eventfd` 等内容，对此感兴趣的读者可阅读原文。\n",
    "\n",
    "大致从Linux内核2.6.27起，凡是会创建文件描述符的syscall一般都增加了额外的`flags`参数，可以直接指定`O_NONBLOCK`和`FD_CLOEXEC`，例如：\n",
    "- accept4-2.6.28\n",
    "- eventfd2-2.6.27\n",
    "- inotify_init1-2.6.27\n",
    "- pipe2-2.6.27\n",
    "- signalfd4-2.6.27\n",
    "- timerfd_create-2.6.25\n",
    "\n",
    "以上6个syscall，除了最后一个是2.6.25的新功能，其余的都是增强原有的调用，把数字尾号去掉就是原来的syscall。\n",
    "\n",
    "`O_NONBLOCK的`功能是开启“非阻塞IO”，而文件描述符默认是阻塞的。这些创建文件描述符的系统调用能直接设定`O_NONBLOCK`选项，其或许能反映当前Linux（服务端）开发的风向，即我在§ 3.3里推荐的one loop per thread+(non-blocking IO with IO multiplexing)。从这些内核改动来看，non-blocking IO已经主流到让内核增加syscall以节省一次`fcntl(2)`调用的程度了。\n",
    "\n",
    "另外，以下新系统调用可以在创建文件描述符时开启`FD_CLOEXEC`选项：\n",
    "- dup3-2.6.27\n",
    "- epoll_create1-2.6.27\n",
    "- socket-2.6.27\n",
    "\n",
    "`FD_CLOEXEC`的功能是让程序`exec()`时，进程会自动关闭这个文件描述符。而文件描述默认是被子进程继承的（这是传统Unix的一种典型IPC，比如用`pipe(2)`在父子进程间单向通信）。\n",
    "\n",
    "以上8个新syscall都允许直接指定`FD_CLOEXEC`，或许说明`fork()`的主要目的已经不再是创建worker process并通过共享的文件描述符和父进程保持通信，而是像Windows的CreateProcess那样创建“干净”的进程（`fork()`之后立刻`exec()`），其与父进程没有多少瓜葛。为了回避`fork() + exec()`之间文件描述符泄漏的race condition，这才在几乎所有能新建文件描述符的系统调用上引入了`FD_CLOEXEC`参数，参见Ulrich Drepper的短文[《Secure File Descriptor Handling》](https://udrepper.livejournal.com/20407.html)\n",
    "\n",
    "以上两个`flags`在我看来，说明Linux服务器开发的主流模型正在由`fork() + worker processes`模型转变为第3章推荐的多线程模型。`fork()`的使用频度会大大降低，将来或许只有专门负责启动别的进程的“看门狗程序”才会调用`fork()`，而一般的网络服务器程序不会再`fork()`出子进程了。原因之一是，`fork()`一般不能在多线程程序中调用(§ 4.9)。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 小结\n",
    "\n",
    "本章只讨论了多线程编程的技术方面，没有讨论设计方面，特别是没有讨论该如何规划一个多线程服务程序的线程数目及用途。我个人遵循的编写多线程C++程序的原则如下：\n",
    "- 线程是宝贵的，一个程序可以使用几个或十几个线程。一台机器上不应该同时运行几百个、几千个用户线程，这会大大增加内核scheduler的负担，降低整体性能。\n",
    "- 线程的创建和销毁是有代价的，一个程序最好在一开始创建所需的线程，并一直反复使用。不要在运行期间反复创建、销毁线程，如果必须这么做，其频度最好能降到1分钟1次（或更低）。\n",
    "- 每个线程应该有明确的职责，例如IO线程（运行`EventLoop::loop()`，处理IO事件）、计算线程（位于`ThreadPool`中，负责计算）等等(p.73)。\n",
    "- 线程之间的交互应该尽量简单，理想情况下，线程之间只用消息传递（例如`BlockingQueue`）方式交互。如果必须用锁，那么最好避免一个线程同时持有两把或更多的锁，这样可彻底防止死锁。\n",
    "- 要预先考虑清楚一个`mutable shared`对象将会暴露给哪些线程，每个线程是读还是写，读写有无可能并发进行。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "C++17",
   "language": "C++17",
   "name": "xcpp17"
  },
  "language_info": {
   "codemirror_mode": "text/x-c++src",
   "file_extension": ".cpp",
   "mimetype": "text/x-c++src",
   "name": "C++17",
   "version": "17"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
